#include <vector>
#include <string>
#include <algorithm>
#include <string.h>
#include <stdlib.h>
#include <time.h>
#ifdef _WIN32
#include <winsock2.h>
#include <windows.h>
#endif
#include "BBS2chProxyHttpHeaders.h"
#include "BBS2chProxyConnection.h"

#ifndef CURLSSLOPT_NATIVE_CA
#define CURLSSLOPT_NATIVE_CA (1<<4)
#endif

extern int allow_chunked;
extern char *proxy_server;
extern long proxy_port;
extern long proxy_type;
extern long timeout;
extern int force_ipv4;
extern CURLSH *curl_share;
extern long curl_tls_version;
extern unsigned int curl_version_number;
extern int tls_verify_cert;
extern std::string tls_ca_bundle;

void *
memmem_priv(const void *l, size_t l_len, const void *s, size_t s_len)
{
	char *cur, *last;
	const char *cl = (const char *)l;
	const char *cs = (const char *)s;
	
	/* we need something to compare */
	if (l_len == 0 || s_len == 0)
		return NULL;
	
	/* "s" must be smaller or equal to "l" */
	if (l_len < s_len)
		return NULL;
	
	/* special case where s_len == 1 */
	if (s_len == 1)
		return (void *)memchr(l, (int)*cs, l_len);
	
	/* the last position where its possible to find "s" in "l" */
	last = (char *)cl + l_len - s_len;
	
	for (cur = (char *)cl; cur <= last; cur++)
		if (cur[0] == cs[0] && memcmp(cur, cs, s_len) == 0)
			return cur;
	
	return NULL;
}

size_t header_callback_proxy(char *buffer, size_t size, size_t nitems, void *userdata)
{
	BBS2chProxyConnection *conn = reinterpret_cast<BBS2chProxyConnection *>(userdata);
	if(conn->status > 0) return size*nitems;
	else if(conn->status == -1) {
		if(!memcmp(buffer,"\r\n",2)) {
			conn->status = 0;
		}
		return size*nitems;
	}
	PBBS2chProxyHttpHeaderEntry parsedHeader = BBS2chProxyHttpHeaders::parse(buffer, size*nitems);
	if (parsedHeader.get()) { // Looks like a header
		const std::string &headerName = parsedHeader->getLowercasedName();
		//fprintf(stderr, "%s\n", parsedHeader->getFull().c_str());
		if (headerName == "connection") {
			conn->socketToClient->writeString("Connection: Close\r\n");
			return size*nitems;
		}
		else if (headerName == "transfer-encoding") {
			if (parsedHeader->contains("chunked")) {
				if (allow_chunked && !conn->isClientHttp1_0) {
					conn->isResponseChunked = true;
					size_t ret = conn->socketToClient->write(buffer, size*nitems);
					return ret;
				}
				return size*nitems;
			}
		}
		else if (conn->force5ch && headerName == "set-cookie") {
			std::string value = parsedHeader->getFull(true);
			std::string lowercasedValue = value;
			std::transform(lowercasedValue.begin(), lowercasedValue.end(), lowercasedValue.begin(), tolower);
			size_t start = lowercasedValue.find("domain=");
			if (start != std::string::npos) {
				start += 7;
				size_t end = value.find(";", start);
				size_t pos = value.find("5ch.net", start);
				if (pos != std::string::npos && (end == std::string::npos || pos < end)) {
					if (pos == start || value[pos-1] == '.') {
						value[pos] = '2';
						conn->socketToClient->writeString(value);
						return size*nitems;
					}
				}
			}
		}
		else if (conn->directDatDownloading && headerName == "content-type") {
			conn->socketToClient->writeString("Content-Type: text/plain\r\n");
			return size*nitems;
		}
		return conn->socketToClient->write(buffer, size*nitems);
	}
	else {
		bool shouldSendExtraHeaders = false;
		if (!strncasecmp("HTTP/", buffer, 5)) {
			const char *ptr = buffer + 5;
			const char *end = buffer + size*nitems;
			while (ptr < end && *ptr != ' ') ptr++;
			while (ptr < end && *ptr == ' ') ptr++;
			if (ptr < end) {
				int code = atoi(ptr);
				if (code == 100) {
					conn->status = -1;
					return size*nitems;
				} else if (code < 400) {
					shouldSendExtraHeaders = true;
				}
			}
		}
		if (!memcmp(buffer, "\r\n", 2)) {
			conn->status = 1;
		}
		size_t ret = conn->socketToClient->write(buffer, size*nitems);
		if (shouldSendExtraHeaders && conn->directDatDownloading == 2) {
			conn->socketToClient->writeString("Thread-Status: 1\r\n");
			conn->socketToClient->writeString("User-Status: 3\r\n");
		}
		return ret;
	}
}

size_t read_callback_proxy(char *buffer, size_t size, size_t nitems, void *userdata)
{
	BBS2chProxyConnection *conn = reinterpret_cast<BBS2chProxyConnection *>(userdata);
	if (size*nitems < 1) return 0;
	if (conn->isClientChunked) {
		size_t chunkSize;
		char tmp[64];
		if (!conn->content_length) {
			if (!conn->socketToClient->readLine(tmp, 64)) return 0;
			chunkSize = strtol(tmp, NULL, 16);
			if (chunkSize == 0) {
				return 0;
			}
		}
		else {
			chunkSize = conn->content_length;
			conn->content_length = 0;
		}
		if (chunkSize <= size*nitems) {
			size_t ret = conn->socketToClient->read(buffer, chunkSize);
			if (ret <= 0) return 0;
			conn->socketToClient->readLine(tmp, 64);
			return ret;
		}
		else {
			size_t ret = conn->socketToClient->read(buffer, size*nitems);
			if (ret <= 0) return 0;
			conn->content_length = chunkSize - ret;
			return ret;
		}
	}
	else if (conn->content_length) {
		size_t bytesToRead = conn->content_length;
		if (size*nitems < conn->content_length) bytesToRead = size*nitems;
		size_t ret = conn->socketToClient->read(buffer, bytesToRead);
		conn->content_length -= ret;
		return ret;
	}
	return 0;
}

size_t write_callback_proxy(char *buffer, size_t size, size_t nitems, void *userdata)
{
	BBS2chProxyConnection *conn = reinterpret_cast<BBS2chProxyConnection *>(userdata);
	if(conn->isResponseChunked) {
		char buf[64];
		snprintf(buf, 64, "%lx\r\n", size*nitems);
		conn->socketToClient->write(buf, strlen(buf));
	}
	size_t ret = conn->socketToClient->write(buffer, size*nitems);
	if(conn->isResponseChunked) conn->socketToClient->writeString("\r\n");
	return ret;
}

size_t header_callback_download(char *buffer, size_t size, size_t nitems, void *userdata)
{
	BBS2chProxyHttpHeaders *headers = static_cast<BBS2chProxyHttpHeaders *>(userdata);
	PBBS2chProxyHttpHeaderEntry parsedHeader = BBS2chProxyHttpHeaders::parse(buffer, size*nitems);
	if (parsedHeader.get()) { // Looks like a header
		const std::string &headerName = parsedHeader->getLowercasedName();
		if (headerName == "connection") {
			if (headers) headers->add(parsedHeader->getName(), "close");
			return size*nitems;
		}
		else if (headerName == "transfer-encoding") {
			if (parsedHeader->contains("chunked")) {
				return size*nitems;
			}
		}
		if (headers) headers->add(parsedHeader->getName(), parsedHeader->getValue());
	}
	else if (headers) headers->setStatusLine(buffer, size*nitems);
	return size*nitems;
}

size_t write_callback_download(char *buffer, size_t size, size_t nitems, void *userdata)
{
	std::vector<char> *data = static_cast<std::vector<char> *>(userdata);
	size_t downloaded = size*nitems;
	if (data) data->insert(data->end(), buffer, buffer+downloaded);
	return downloaded;
}

double getCurrentTime(void)
{
#ifdef _WIN32
	FILETIME ft;
	GetSystemTimeAsFileTime(&ft);
	unsigned long long now = ft.dwHighDateTime;
	now <<= 32;
	now |= ft.dwLowDateTime;
	return (double)(now * 1e-7 - 11644473600.0);
#else
	struct timeval tv;
	gettimeofday(&tv, NULL);
	return (double)(tv.tv_sec + tv.tv_usec * 1e-6);
#endif
}

void configureCurlHandle(CURL *curl)
{
	curl_easy_setopt(curl, CURLOPT_NOSIGNAL, 1L);
	curl_easy_setopt(curl, CURLOPT_TIMEOUT, timeout);
	curl_easy_setopt(curl, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_1);
	curl_easy_setopt(curl, CURLOPT_SSLVERSION, curl_tls_version);
	if (curl_share) curl_easy_setopt(curl, CURLOPT_SHARE, curl_share);
	if (force_ipv4) curl_easy_setopt(curl, CURLOPT_IPRESOLVE, CURL_IPRESOLVE_V4);
	if (proxy_server) {
		curl_easy_setopt(curl, CURLOPT_PROXY, proxy_server);
		curl_easy_setopt(curl, CURLOPT_PROXYPORT, proxy_port);
		curl_easy_setopt(curl, CURLOPT_PROXYTYPE, proxy_type);
	}
	if (!tls_verify_cert) {
		curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 0L);
		curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 0L);
	}
	else {
		if (!tls_ca_bundle.empty() && (tls_verify_cert & 0x1)) {
			curl_easy_setopt(curl, CURLOPT_CAINFO, tls_ca_bundle.c_str());
		}
#if LIBCURL_VERSION_NUM >= 0x071900 // 7.25.0 or later
		if (tls_verify_cert & 0x2) {
			curl_easy_setopt(curl, CURLOPT_SSL_OPTIONS, CURLSSLOPT_NATIVE_CA);
		}
#endif
	}
	//curl_easy_setopt(curl, CURLOPT_VERBOSE, 1L);
}

int getCurrentTlsBackend(CURL *curl)
{
	int backend = 0;
#if LIBCURL_VERSION_NUM >= 0x073000 // 7.48.0 or later
	if (curl_version_number >= 0x073000) {
		// CURLINFO_TLS_SSL_PTR returns a correct result without making any connections
		struct curl_tlssessioninfo *info;
		if (!curl) {
			curl = curl_easy_init();
		    curl_easy_getinfo(curl, CURLINFO_TLS_SSL_PTR, &info);
		    if (info) backend = info->backend;
		    curl_easy_cleanup(curl);
		} else {
		    curl_easy_getinfo(curl, CURLINFO_TLS_SSL_PTR, &info);
		    if (info) backend = info->backend;
		}
	}
#endif
	return backend;
}
